跳到主要内容

Go 语言并发学习-sync 包-锁

Go 中的锁

虽然 Go 推荐使用 channel 进行线程之间的同步,但是有些场景还是使用锁比较方便。

Go 语言包中的 sync 包提供了两种锁类型:sync.Mutexsync.RWMutex,前者是互斥锁,后者是读写锁。

type Locker interface {
Lock()
Unlock()
}

互斥锁

标准库代码包 sync 中的 Mutex 结构体类型代表。只有两个公开方法:调用 Lock() 获得锁,调用 unlock() 释放锁。

使用 Lock() 加锁后,不能再继续对其加锁(同一个 goroutine 中,即:同步调用),否则会 panic。只有在 unlock() 之后才能再次 Lock()。异步调用 Lock(),是正当的锁竞争(即其它协程中调用),当然不会有 panic 了。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。

func (m *Mutex) Unlock() 用于解锁 m,如果在使用 Unlock() 前未加锁,就会引起一个运行错误。

使用例:

var lck sync.Mutex
func foo() {
lck.Lock()
defer lck.Unlock()
// ...
}

Mutex 也可以作为 struct 的一部分,这样这个 struct 就会防止被多线程更改数据。如下,编写一个原子计数器

package main

import (
"fmt"
"sync"
"time"
)

// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}

// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}

// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}

func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}

time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}

读写锁

读写锁和互斥锁不同之处在于,可以分别针对读操作和写操作进行分别锁定,这样对于性能有一定的提升。读写锁,对于多个写操作,以及写操作和读操作之前都是互斥的这一点基本等同于互斥锁。但是对于同时多个读操作之前却非互斥关系,这也是相读写锁性能高于互斥锁的主要原因。

基本遵循原则(其实就是读写锁的基本用途):

  • 写锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞;而且读锁与写锁之间是互斥的;
  • 读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞;
  • 对未被写锁定的读写锁进行写解锁,会引发 Panic;
  • 对未被读锁定的读写锁进行读解锁的时候也会引发 Panic;
  • 写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的 goroutine;
  • 读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的 goroutine。

RWMutex 提供四个方法:

func (*RWMutex) Lock // 写锁定
func (*RWMutex) Unlock // 写解锁
func (*RWMutex) RLock // 读锁定
func (*RWMutex) RUnlock // 读解锁

使用示例:

package main

import (
"fmt"
)

var m *sync.RWMutex

func main() {
wg := sync.WaitGroup{}
wg.Add(20)
var rwMutex sync.RWMutex
Data := 0
for i := 0; i < 10; i++ {
go func() {
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("Read data: %v\n", Data)
wg.Done()
time.Sleep(2 * time.Second)
// 这句代码第一次运行后,读解锁。
// 循环到第二个时,读锁定后,这个goroutine就没有阻塞,同时读成功。
}()

go func(t int) {
rwMutex.Lock()
defer rwMutex.Unlock()
Data += t
fmt.Printf("Write Data: %v %d \n", Data, t)
wg.Done()
// 这句代码让写锁的效果显示出来,写锁定下是需要解锁后才能写的。
time.Sleep(2 * time.Second)
}(i)
}
time.Sleep(5 * time.Second)
wg.Wait()
}

WaitGroup 等待一组协程

官方文档对 WaitGroup 的描述是:一个 WaitGroup 对象可以等待一组协程结束。其实就是 Java 中的 CountDownLatch

  1. main 协程通过调用 wg.Add(delta int) 设置 worker 协程的个数,然后创建 worker 协程;
  2. worker 协程执行结束以后,都要调用 wg.Done()
  3. main 协程调用 wg.Wait() 且被 block,直到所有 worker 协程全部执行结束后返回。

使用例如下:

// src/cmd/compile/internal/ssa/gen/main.go
func main() {
// 省略部分代码 ...
var wg sync.WaitGroup

for _, task := range tasks {
task := task
wg.Add(1)
go func() {
task()
wg.Done()
}()
}

wg.Wait()
// 省略部分代码...
}

如何正确的使用读写锁

抛开业务来理解读写锁,它的本质是:

  • Lock() 时,会阻塞另一个协程 Rlock()Lock()
  • Rlock() 时,不会阻塞另一个协程 Rlock()。但是会阻塞另一个协程的 Lock()

如下使用,读的操作都加读锁,它可以阻塞写锁

package main

import (
"fmt"
"sync"
"time"
)

type SafeInteger struct {
m sync.RWMutex
data int
}

func (this *SafeInteger) readData() int {
this.m.RLock()
defer this.m.RUnlock()

time.Sleep(1 * time.Second)
return this.data
}

func (this *SafeInteger) writeData(val int) {
this.m.Lock()
defer this.m.Unlock()
this.data = val

time.Sleep(1 * time.Second)
fmt.Println("Write:", this.data)
}

func main() {
si := &SafeInteger{data: 10}

// 开 10 个协程用来读
for i := 0; i < 10; i++ {
go func() {
fmt.Println("Read:", si.readData())
}()
}

// 开 10 个协程用来读
for i := 0; i < 10; i++ {
go func(i int) {
si.writeData(i * 5)
}(i)
}
// 阻塞
select {}
}

如上代码,运行时可以发现读的操作不会被阻塞,写的操作才会被阻塞

锁的使用注意点

mutex 实例无需实例化,声明即可使用

func add(){
var mutex sync.Mutex
mutex.Lock()
defer mutex.Unlock()
fmt.Println("test lock")
}

mutex 在传递给外部使用的时候,需要传指针,不然传的是拷贝,会引起锁失败。并且指针的 mutex 是一定要实例化过的。

func add() *sync.Mutex {
var m = &sync.Mutex{}
return m
}

对同一个锁,进行多次锁,会死锁

func a(){
var mutex sync.Mutex
mutex.Lock()
mutex.Lock() // dead lock
}

对一个 RWLock 进行同时 Lock()RLock() 会死锁.

func a(){
var mutex sync.RWMutex
mutex.RLock()
mutex.Lock() // dead lock
}

Go 中的重入锁 🚧

TODO: ...

如下代码函数 F()G() 使用了相同的互斥锁,并且都在各自函数内部进行了加锁,这要使用就会出现死锁

func F() {
mu.Lock()
//... do some stuff ...
G()
//... do some more stuff ...
mu.Unlock()
}

func G() {
mu.Lock()
//... do some stuff ...
mu.Unlock()
}

Reference